Skip to content

feat(power): "Low refresh" hourly sync, a sub-option of Power saving - #1430

Merged
ryanbr merged 1 commit into
mainfrom
feat/low-refresh-sync
Aug 18, 2026
Merged

feat(power): "Low refresh" hourly sync, a sub-option of Power saving#1430
ryanbr merged 1 commit into
mainfrom
feat/low-refresh-sync

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Adds "Low refresh" as a sub-option of Settings → Power saving, beside the existing battery threshold and "Pause HRV capture" levers. Idea from @cbedsole03.

Unlike the battery lever it is not charge-gated: once chosen, 60 min becomes the BASE cadence the other levers stretch from, at any strap charge.

Why

Fewer periodic offloads means fewer reconnect bursts — the measured WHOOP 4.0 drain mechanism (#1120/#1144). A strap log from a 10.1.1 staging build showed 52 periodic offloads in one session, 26 of them idle-timeout stalls, each holding the link and reconnecting for little or no new data.

Composition

Every lever now stretches from the low-refresh base with max, so a lever can only make the cadence quieter, never restore a faster one the user asked to slow down. The 5/MG empty-history stretch (45 min) and the low-battery stretch (45 min) are both shorter than 60 min, so they become no-ops rather than speed-ups. Default off — with the toggle off every existing path is byte-identical.

Deliberately cadence-only (and why)

@cbedsole03's version also stretched the keep-alive 30s → 300s and released continuous HRV capture. Both are dropped here, because they cost data rather than delaying it:

So what ships delays sync into larger batches and loses nothing: the strap banks to flash and only trims on our ack.

Parity

Android twin in the same PR — NoopPrefs.lowRefresh, the LOW_REFRESH_BACKFILL_INTERVAL_MS/baseBackfillIntervalMs pair, the same max() composition in nextBackfillDelayMs, and the same sub-toggle inside the Settings if (powerSaving) block. Both platforms apply on the next re-arm (matching each side's existing battery lever), so a sync in flight is never cut. New copy localized de/es/fr/pl/pt-PT/zh (Android) and de/es/fr/pl/pt-PT (iOS catalog).

Validation

  • Full Android suite: 4,042 tests / 490 classes, 0 failures (5 new), plus compileFullDebugKotlin, processFullDebugResources (aapt) and i18n_audit.py --ci green.
  • Swift half is app-target → validated by app-build. Note StrandTests don't run in default CI, so the Swift twin of the cadence test is compile-checked there rather than executed; the Kotlin twin pins the same five cases and does execute.

@ryanbr
ryanbr force-pushed the feat/low-refresh-sync branch from e838300 to 98e59b3 Compare August 18, 2026 10:28
Adds a user-elected hourly background-offload cadence under Settings -> Power
saving, beside the existing battery threshold and "Pause HRV capture" levers.
Unlike the battery lever it is not charge-gated: once chosen, 60 min is the
BASE the other levers stretch from, at any strap charge.

Fewer periodic offloads means fewer reconnect bursts, which is the measured
WHOOP 4.0 drain mechanism (#1120/#1144) - a strap log from a 10.1.1 staging
build showed 52 periodic offloads in a session, 26 of them idle-timeout stalls,
each holding the link and re-connecting for little or no new data.

Composition: every lever now stretches from the low-refresh base with `max`, so
a lever can only make the cadence quieter, never restore a faster one the user
asked to slow down. The 5/MG empty-history stretch (45 min) and the low-battery
stretch (45 min) are both shorter than 60 min, so they become no-ops rather than
speed-ups. Default off; with the toggle off every existing path is byte-identical.

DELIBERATELY CADENCE-ONLY - the idea comes from @cbedsole03, whose version also
stretched the keep-alive 30s -> 300s and released continuous HRV capture. Both
are dropped here because they cost data rather than delaying it:

- The keep-alive tick is not just a ping: it re-arms the WHOOP 4 realtime burst
  (R10/R11) every cycle so streaming cannot lapse, and it evaluates the stall
  fuse, which is 120s on a 4.0. A 300s tick under-samples that fuse, so a
  stalled 4.0 link would sit stalled up to 5 min - prolonging exactly the
  reconnect churn this feature exists to reduce. `startKeepAlive` already
  documents that the battery gain there "rounds to nothing on a 30 s timer".
- Releasing continuous HRV capture drops dense beat-to-beat R-R while it is on,
  INCLUDING overnight, which is where that stream exists to be dense. Density
  not captured cannot be backfilled later, and it would silently override the
  user`s own Continuous-HRV-capture preference. "Pause HRV capture" (#477) and
  the #927 overnight window are the levers for that, and they stay in charge.

So the shipped feature delays sync into larger batches and loses nothing: the
strap banks to flash and only trims on our ack.
PARITY: the Android twin lands in the same PR — NoopPrefs.lowRefresh, the
WhoopBleClient LOW_REFRESH_BACKFILL_INTERVAL_MS/baseBackfillIntervalMs pair, the
same max() composition inside nextBackfillDelayMs, and the same sub-toggle inside
the Settings if(powerSaving) block. Both platforms apply on the NEXT re-arm
(matching each side's existing battery lever), so a sync in flight is never cut.
New copy localized de/es/fr/pl/pt-PT/zh on Android and de/es/fr/pl/pt-PT in the
iOS catalog.

Tests: LowRefreshCadenceTests (Swift) + LowRefreshCadenceTest (Kotlin) pin the
same five cases — base on/off, and the three composition cases (low-battery
lever, 5/MG empty-history stretch, unchanged default path).

Validation: full Android suite 4042 tests / 490 classes green (5 new);
compileFullDebugKotlin + processFullDebugResources + i18n --ci green. The Swift
half is app-target, so it is validated by app-build; note StrandTests do not run
in default CI, so the Swift twin of the cadence test is compile-checked there
rather than executed.
@ryanbr
ryanbr force-pushed the feat/low-refresh-sync branch from 98e59b3 to 80c51f3 Compare August 18, 2026 10:34
@ryanbr
ryanbr merged commit 83596b6 into main Aug 18, 2026
5 checks passed
@ryanbr
ryanbr deleted the feat/low-refresh-sync branch August 18, 2026 11:16
ryanbr added a commit that referenced this pull request Aug 18, 2026
Build numbers only. MARKETING_VERSION / versionName stay 10.1.1 — the staging
line is one above the latest release (v10.1.0) and holds there until 10.1.1
actually ships.

versionCode 341 -> 342 and CURRENT_PROJECT_VERSION 223 -> 224, both +1 past the
previous staging build so a sideloaded update installs in place rather than
being refused as a downgrade.

Picks up since the last testing build (f2271cd): backup build provenance
(#1422), the CTL/ATL/TSB training-load engine (#1423) and its Trends card
(#1425), sleep-vs-wake HR contrast (#1424), the v20 optical decoder + CRC gate
(#895), the Sleep UI split (#1426), the WHOOP4 clock-correlation fix (#1427),
the settings waist-row layout + copy fix (#1428), hourly Apple step import
(#1429), Low refresh (#1430), and Power saving moving into More with a 35%
ceiling (#1431).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant